// SPDX-License-Identifier: GPL-2.0
/*
 * VPMEM Numa Support
 *
 * Copyright (C) 2020 HUAWEI, Inc.  All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <linux/acpi.h>
#include <linux/numa.h>
#include <linux/kernel.h>
#ifdef CONFIG_ARM64
#include <asm/vpmem/api.h>
#else
#include <asm/e820/api.h>
#endif

#include <linux/vpmem_numa.h>
#include <linux/vpmem_metadata.h>

static struct vpmem_table *vpmem_numa_table_init(void)
{
	struct vpmem_table *table;
	struct vpmem_entry *entry;
	u64 start = 0;
	u64 size = 0;

	size = e820__mapped_find(&start, E820_TYPE_VPMEM);
	if (!size) {
		VPMEM_WARN("virtual machine RAM not found in e820\n");
		return ERR_PTR(-ENOENT);
	}

	table = kzalloc(sizeof(*table), GFP_KERNEL);
	if (!table)
		return ERR_PTR(-ENOMEM);

	while (size) {
		if (table->nr_entry < NR_VPMEM_NODE_MEMBLKS) {
			entry = &table->entry[table->nr_entry];
			entry->start = start;
			entry->end = start + size;
			entry->numa = 0;
			table->nr_entry += 1;
		}
		start += size;
		size = e820__mapped_find(&start, E820_TYPE_VPMEM);
	}
	return table;
}

static int vpmem_numa_table_add_mem(struct vpmem_table *table, u64 start,
		u64 size, u32 node)
{
	int x;

	for (x = 0; x < table->nr_mem_entry; x++) {
		if (node != table->mem_entry[x].numa)
			continue;

		if (table->mem_entry[x].start == (start + size)) {
			table->mem_entry[x].start = start;
			return 0;
		}

		if (table->mem_entry[x].end == start) {
			table->mem_entry[x].end += size;
			return 0;
		}
	}

	x = table->nr_mem_entry;
	if (x >= ARRAY_SIZE(table->mem_entry))
		return -ENOSPC;

	table->mem_entry[x].start = start;
	table->mem_entry[x].end = start + size;
	table->mem_entry[x].numa = node;
	table->nr_mem_entry++;
	return 0;
}

void vpmem_numa_table_debug(struct vpmem_table *table)
{
	u32 i;
	struct vpmem_entry *entry;

	if (IS_ERR(table)) {
		VPMEM_ERR("invalid vpmem table %ld\n", PTR_ERR(table));
		return;
	}

	for (i = 0; i < table->nr_mem_entry; i++) {
		entry = &table->mem_entry[i];
		VPMEM_INFO("ACPI Node %d [0x%pK 0x%pK]\n",
			entry->numa, (void *)entry->start, (void *)entry->end);
	}

	for (i = 0; i < table->nr_entry; i++) {
		entry = &table->entry[i];
		VPMEM_INFO("VPMEM Node %d [0x%pK 0x%pK]\n",
			entry->numa, (void *)entry->start, (void *)entry->end);
	}
}

static int vpmem_numa_table_insert(struct vpmem_table *table, u32 pos)
{
	struct vpmem_entry *entry;
	size_t size;

	if (table->nr_entry >= NR_VPMEM_NODE_MEMBLKS)
		return -1;

	if (pos >= table->nr_entry)
		return -1;

	size = (table->nr_entry - pos) * sizeof(*entry);
	entry = &table->entry[pos];
	memmove(entry + 1, entry, size);
	table->nr_entry += 1;
	return 0;
}

static long vpmem_numa_table_update_affinity(struct vpmem_table *table,
			u64 start, u64 size, u32 numa)
{
	u32 i;
	u64 end = start + size;
	struct vpmem_entry *entry;

	for (i = 0; i < table->nr_entry; i++) {
		entry = &table->entry[i];
		if (entry->start >= end || entry->end <= start)
			continue;

		if (entry->start < start) {
			if (vpmem_numa_table_insert(table, i))
				return -ENOMEM;

			entry->end = start;
			entry++;
			entry->start = start;
			continue;
		}

		if (entry->end > end) {
			if (vpmem_numa_table_insert(table, i))
				return -ENOMEM;

			entry->end = end;
			entry->numa = numa;
			entry++;
			entry->start = end;
			continue;
		}

		entry->numa = numa;
	}
	return 0;
}

#define TABLE_HEADER_SIZE (sizeof(struct acpi_table_srat))
#define ENTRY_HEADER(addr) ((struct acpi_subtable_header *)(addr))
#define FIRST_ENTRY(header) ENTRY_HEADER((unsigned long)header + TABLE_HEADER_SIZE)
#define NEXT_ENTRY(entry) ENTRY_HEADER((unsigned long)entry + entry->length)
#define ENTRY_DATA(entry) ((unsigned long)entry + sizeof(struct acpi_subtable_header))

struct vpmem_table *vpmem_numa_init(void)
{
	struct vpmem_table *table;
	struct acpi_table_header *header = NULL;
	struct acpi_subtable_header *entry;
	struct acpi_srat_mem_affinity *mem;
	acpi_size table_end;
	acpi_status status;
	int node;
	long ret = 0;

	table = vpmem_numa_table_init();
	if (IS_ERR(table))
		return table;

	if (acpi_disabled) {
		VPMEM_INFO("acpi disabled, disable vpmem numa\n");
		vpmem_numa_table_debug(table);
		return table;
	}

	status = acpi_get_table(ACPI_SIG_SRAT, 0, &header);
	if (status == AE_NOT_FOUND) {
		VPMEM_INFO("numa disabled, disable vpmem numa\n");
		vpmem_numa_table_debug(table);
		return table;
	}

	if (!header) {
		VPMEM_WARN("get acpi table error(%d), disable vpmem numa\n", status);
		kfree_sensitive(table);
		return ERR_PTR(-ENODEV);
	}

	table_end = (unsigned long)header + header->length;

	entry = FIRST_ENTRY(header);
	while (ENTRY_DATA(entry) < table_end) {
		if (entry->type != ACPI_SRAT_TYPE_MEMORY_AFFINITY) {
			entry = NEXT_ENTRY(entry);
			continue;
		}

		mem = (struct acpi_srat_mem_affinity *)entry;
		if (!(mem->flags & ACPI_SRAT_MEM_ENABLED) ||
			(mem->flags & ACPI_SRAT_MEM_HOT_PLUGGABLE) ||
			(mem->flags & ACPI_SRAT_MEM_NON_VOLATILE)) {
			entry = NEXT_ENTRY(entry);
			continue;
		}

		node = pxm_to_online_node(mem->proximity_domain);
		ret = vpmem_numa_table_add_mem(table, mem->base_address,
				mem->length, node);
		if (ret) {
			kfree_sensitive(table);
			table = ERR_PTR(ret);
			break;
		}

		if (node == 0) {
			entry = NEXT_ENTRY(entry);
			continue;
		}

		ret = vpmem_numa_table_update_affinity(table,
				mem->base_address, mem->length, node);
		if (ret) {
			kfree_sensitive(table);
			table = ERR_PTR(ret);
			break;
		}

		entry = NEXT_ENTRY(entry);
	}

	acpi_put_table(header);
	vpmem_numa_table_debug(table);
	return table;
}

void vpmem_numa_get_stat(struct vpmem_table *table,
		struct vpmem_stat *stat)
{
	struct vpmem_entry *entry;
	unsigned long hugepages;
	u32 i;

	for (i = 0; i < table->nr_mem_entry; i++) {
		entry = &table->mem_entry[i];
		if (entry->numa >= ARRAY_SIZE(stat->total)) {
			VPMEM_WARN("ACPI [0x%llx 0x%llx] Node %d invalid\n",
					entry->start, entry->end, entry->numa);
			continue;
		}
		hugepages = (entry->end - entry->start) >> VPMEM_HUGE_PAGE_SHIFT;
		stat->total[entry->numa] += hugepages;
		if (stat->nr_node < entry->numa)
			stat->nr_node = entry->numa;
	}

	for (i = 0; i < table->nr_entry; i++) {
		entry = &table->entry[i];
		if (entry->numa >= ARRAY_SIZE(stat->vpmem)) {
			VPMEM_WARN("VPMEM [0x%llx 0x%llx] Node %d invalid\n",
					entry->start, entry->end, entry->numa);
			continue;
		}
		hugepages = (entry->end - entry->start) >> VPMEM_HUGE_PAGE_SHIFT;
		stat->vpmem[entry->numa] += hugepages;
		if (stat->nr_node < entry->numa)
			stat->nr_node = entry->numa;
	}
}
